﻿#if !WINDOWS_PHONE
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace JGLib.Settings.Records
{
    public class RecordFile : XnaFile
    {
        private Record[] Records;

        /// <summary>Initializes an instance of a RecordFile.</summary>
        /// <param name="recordFilePath">The name of the record file.</param>
        /// <param name="records">The list of records that will be written and read from the record file.</param>
        public RecordFile(string recordFilePath, params Record[] records)
            : base(recordFilePath)
        {
            this.Records = records;
            this.Refresh();
        }

        protected override void WriteDefaultText(Stream stream)
        {
            using (StreamWriter writer = new StreamWriter(stream))
                foreach (Record record in this.Records)
                    record.writeDefaultToStream(writer);
        }
        protected override void WriteText(Stream stream)
        {
            using (StreamWriter writer = new StreamWriter(stream))
                foreach (Record record in this.Records)
                    record.writeToStream(writer);
        }
        protected override void ReadText(Stream stream)
        {
            using (StreamReader reader = new StreamReader(stream))
                this.Records = Record.ReadFromStream(reader);
        }

        /// <summary>Resets the specified record to its default value.</summary>
        /// <param name="record">The record to revert to its default value.</param>
        public void ResetRecord(Record record)
        {
            record.Data = record.DefaultData;
            this.Flush();
        }
        /// <summary>Resets the specified record and all of its children to their default values.</summary>
        /// <param name="record">The record to revert to its default value.</param>
        public void ResetRecordAndChildren(Record record)
        {
            this.resetRecordRec(record);
            this.Flush();
        }
        private void resetRecordRec(Record record)
        {
            record.Data = record.DefaultData;
            foreach (Record child in record.Children)
                this.resetRecordRec(child);
        }
        /// <summary>Resets all records to their default values.</summary>
        public void ResetAllRecords()
        {
            foreach (Record record in this.Records)
                this.resetRecordRec(record);
            this.Flush();
        }
    }

    /// <summary>A record of key: value pairs.</summary>
    public class Record : IEnumerable<Record>
    {
        /// <summary>The header name of this record.</summary>
        public string Name { get; set; }
        /// <summary>The header data of this record.</summary>
        public string Data { get; internal set; }
        /// <summary>The default header data of this record.</summary>
        public string DefaultData { get; set; }
        /// <summary>The sub-records of this record.</summary>
        public Record[] Children { get; internal set; }
        /// <summary>The default sub-records of this record.</summary>
        public Record[] DefaultChildren { get; set; }

        private int numLines;

        /// <summary>Initializes a new record with a specified header name, header data, and sub-records.</summary>
        /// <param name="name">The header name.</param>
        /// <param name="defaultData">The header data.</param>
        /// <param name="defaultChildren">The sub-records.</param>
        public Record(string name, string defaultData, params Record[] defaultChildren)
        {
            this.Name = name;
            this.DefaultData = defaultData;
            this.Data = defaultData;
            this.DefaultChildren = defaultChildren;
            this.Children = defaultChildren;
        }
        /// <summary>Creates a new record with specified header name and sub-records.</summary>
        /// <param name="name">The header name.</param>
        /// <param name="children">The sub-records.</param>
        public Record(string name, params Record[] children) : this(name, null, children) { }

        /// <summary>Gets the sub-record at a specified index.</summary>
        /// <param name="index">The 0-based index of the sub-record.</param>
        /// <returns>The sub-record at the specified index.</returns>
        public Record this[int index]
        {
            get
            {
                return Children[index];
            }
        }
        /// <summary>Gets the first sub-record with a specified header name.</summary>
        /// <param name="name">The sub-record's name.</param>
        /// <returns>The sub-record with the specified header name.</returns>
        public Record this[string name]
        {
            get
            {
                foreach (Record child in Children)
                    if (child.Name == name)
                        return child;
                throw new KeyNotFoundException("Could not find sub-record with name '" + name + "'.");
            }
        }

        /// <summary>Enumerates only the children sub-records of the current Record.</summary>
        /// <returns>A Record enumerator.</returns>
        public IEnumerator<Record> GetEnumerator()
        {
            foreach (Record child in Children)
                yield return child;
        }
        [Obsolete("It's like drugs. Just don't do it.", true)]
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            throw new NotImplementedException();
        }

        /// <summary>Recursively enumerates all Records in the current Record.</summary>
        public IEnumerable<Record> AllRecords
        {
            get
            {
                yield return this;
                foreach (Record child in Children)
                    foreach (Record record in child.AllRecords)
                        yield return record;
            }
        }

        /// <summary>Parses a series of Records from lines of text.</summary>
        /// <param name="lines">The lines of text.</param>
        /// <returns>An array of parsed Records.</returns>
        internal static Record[] ReadFromLines(params string[] lines)
        {
            lines = (from line in lines
                     where line.Trim(' ').Length != 0
                     select line).ToArray();
            List<Record> records = new List<Record>();
            int index = 0;
            while (index < lines.Length)
            {
                Record record = parse(lines, index, 0);
                index += record.numLines;
            }
            return records.ToArray();
        }
        /// <summary>Parses a series of Records from the text of a file.</summary>
        /// <param name="filePath">The path to the file to parse.</param>
        /// <returns>An array of parsed Records.</returns>
        internal static Record[] ReadFromFile(string filePath)
        {
            return Record.ReadFromLines(File.ReadAllLines(filePath));
        }
        /// <summary>Parses a series of Records from a stream of text.</summary>
        /// <param name="reader">The TextReader to parse Records from.</param>
        /// <returns>An array of parsed Records.</returns>
        internal static Record[] ReadFromStream(TextReader reader)
        {
            List<string> lines = new List<string>();
            while (reader.Peek() >= 0)
                lines.Add(reader.ReadLine());
            return Record.ReadFromLines(lines.ToArray());
        }

        private static Record parse(string[] lines, int index, int indent)
        {
            int startLine = index;

            string headerLine = lines[index];

            int headerIndent = headerLine.Indent();
            if (headerIndent >= indent)
            {
                indent = headerIndent;

                string[] header = parseHeader(headerLine);
                if (header == null)
                    throw new InvalidHeaderException(index, headerLine);
                string name = header[0], data = header[1];

                List<Record> children = new List<Record>();

                index++;
                while (index < lines.Length)
                {
                    Record child = Record.parse(lines, index, indent + 1);
                    if (child == null)
                        break;
                    children.Add(child);
                    index += child.numLines;
                }

                Record record = new Record(name, data, children.ToArray());
                record.numLines = index - startLine;
                return record;
            }
            return null;
        }
        private static string[] parseHeader(string line)
        {
            line = line.TrimStart(' ');
            int colonIndex = line.IndexOf(':');
            if (colonIndex < 0)
                return null;
            string name = line.Substring(0, colonIndex);
            if (name.Contains(" "))
                return null;
            if (line.Length <= colonIndex + 1)
                return new string[] { name, null };
            if (line[colonIndex + 1] != ' ')
                return null;
            string data = line.Substring(colonIndex + 2);
            return new string[] { name, data };
        }

        internal string[] lines(int indentSize)
        {
            return this.lines(indentSize, false);
        }
        /// <summary>Computes a set of lines of text suitable for parsing as a Record.</summary>
        /// <param name="indentSize">The number of spaces in each indent level.</param>
        /// <returns>A set of lines.</returns>
        internal string[] lines(int indentSize, bool defaultValues)
        {
            if (indentSize <= 0)
                throw new ArgumentException("The indent size cannot be less than or equal to zero.", "indentSize");
            return getLines(indentSize, 0, defaultValues).ToArray();
        }
        private List<string> getLines(int indentSize, int level)
        {
            return this.getLines(indentSize, level, false);
        }
        private List<string> getLines(int indentSize, int level, bool defaultValues)
        {
            List<string> lines = new List<string>();
            lines.Add(' '.Times(indentSize * level) + (defaultValues ? getFormattedDefaultHeader() : GetFormattedHeader()));
            foreach (Record child in Children)
                lines.AddRange(child.getLines(indentSize, level + 1));
            return lines;
        }

        internal void writeToFile(string filePath)
        {
            writeToFile(filePath, 2);
        }
        /// <summary>Writes the current Record to a specified file.</summary>
        /// <param name="filePath">The path of the file to write to.</param>
        /// <param name="indentSize">The number of spaces in each indent level; default is 2.</param>
        internal void writeToFile(string filePath, int indentSize)
        {
            File.WriteAllLines(filePath, lines(indentSize));
        }
        internal void writeToStream(TextWriter writer)
        {
            writeToStream(writer, 2);
        }
        /// <summary>Writes the current Record to a specified stream.</summary>
        /// <param name="writer">The TextWriter to write to.</param>
        /// <param name="indentSize">The number of spaces in each indent level; default is 2.</param>
        internal void writeToStream(TextWriter writer, int indentSize)
        {
            string[] text = lines(indentSize);
            foreach (string line in text)
                writer.WriteLine(line);
        }
        internal void writeDefaultToStream(TextWriter writer)
        {
            writeDefaultToStream(writer, 2);
        }
        internal void writeDefaultToStream(TextWriter writer, int indentSize)
        {
            string[] text = lines(indentSize, true);
            foreach (string line in text)
                writer.WriteLine(line);
        }

        public override string ToString()
        {
            string str = GetFormattedHeader();
            if (Children.Length != 0)
            {
                str += " {" + Children.Length + " child";
                if (Children.Length > 1)
                    str += "ren";
                str += "}";
            }
            return str;
        }
        /// <summary>Computes a formatted header string of this Record.</summary>
        /// <returns>The header, formatted as a string.</returns>
        public string GetFormattedHeader()
        {
            if (Data == null) return Name + ":";
            return Name + ": " + Data;
        }
        private string getFormattedDefaultHeader()
        {
            if (DefaultData == null) return Name + ":";
            return Name + ": " + DefaultData;
        }
    }

    /// <summary>A record header could not be parsed due to incorrect format.</summary>
    public class InvalidHeaderException : Exception
    {
        /// <summary>The 0-based index of the error-causing line.</summary>
        public int LineNumber { get; private set; }
        /// <summary>The text of the error-causing line.</summary>
        public string Line { get; private set; }

        /// <summary>Constructs an InvalidHeaderException, with a specified line number and line text.</summary>
        /// <param name="lineNumber">The 0-based line index of the invalid line.</param>
        /// <param name="line">The text of the line with the invalid header.</param>
        public InvalidHeaderException(int lineNumber, string line)
            : base("Could not parse record header on line " + (lineNumber + 1) + ".")
        {
            this.LineNumber = lineNumber;
            this.Line = line;
        }
    }
}
#endif